#!/usr/bin/python

#
# This file is part of the batocera distribution (https://batocera.org).
# Copyright (c) 2022 Nicolas Adenis-Lamarre.
#
# This program is free software: you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation, version 3.
#
# You should have received a copy of the GNU General Public License 
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# YOU MUST KEEP THIS HEADER AS IT IS
#
#

import evdev
from evdev import ecodes
import select
import sys
import time
import os
import hashlib
import math

if len(sys.argv) != 2 and len(sys.argv) != 3:
    print("mising device argument")
    exit(1)

if len(sys.argv) == 3:
    bCalibration = int(sys.argv[2])
else:
    bCalibration = ecodes.BTN_RIGHT

# input
devpath = sys.argv[1]
input = evdev.InputDevice(devpath)
absVals = [[0, 100], [0, 100]] # input rectangle
for (code, inf) in input.capabilities()[ecodes.EV_ABS]:
    if code == ecodes.ABS_X:
        absVals[0] = [inf.min, inf.max]
    if code == ecodes.ABS_Y:
        absVals[1] = [inf.min, inf.max]
poll = select.poll()
poll.register(input.fd, select.POLLIN)
inputkey = hashlib.md5(input.name.encode()).hexdigest()[0:8]

# output
outputSize = [1000, 1000] # output rectangle
evkeys = []
for x in range(ecodes.KEY_MAX):
    if x not in [
            ecodes.BTN_DIGI,
            ecodes.BTN_TOOL_PEN,
            ecodes.BTN_TOOL_RUBBER,
            ecodes.BTN_TOOL_BRUSH,
            ecodes.BTN_TOOL_PENCIL,
            ecodes.BTN_TOOL_AIRBRUSH,
            ecodes.BTN_TOOL_FINGER,
            ecodes.BTN_TOOL_MOUSE,
            ecodes.BTN_TOOL_LENS,
            ecodes.BTN_TOUCH,
            ecodes.BTN_STYLUS,
            ecodes.BTN_STYLUS2,
            ecodes.BTN_TOOL_DOUBLETAP,
            ecodes.BTN_TOOL_TRIPLETAP,
            ecodes.BTN_TOOL_QUADTAP]: # avoid some buttons causing issues to libinput
        evkeys.append(x)

target = evdev.UInput(name=input.name+" calibrated", events={
    ecodes.EV_ABS: [
        (ecodes.ABS_X, evdev.AbsInfo(value=0, min=0, max=outputSize[0], fuzz=0, flat=0, resolution=0)),
        (ecodes.ABS_Y, evdev.AbsInfo(value=0, min=0, max=outputSize[1], fuzz=0, flat=0, resolution=0))
    ],
    ecodes.EV_KEY: evkeys})

needSync = False
calibration_timeChecker = None
calibration_mode = False
calibration_step = 0
pLast = [0, 0]
pCenter = [0, 0]
pInputWindow = [absVals[0][1]-absVals[0][0], absVals[1][1]-absVals[1][0]]

def moveTargetTo(fromPosition, toPosition):
    currentPosition = fromPosition
    while fromPosition[0] != toPosition[0] or fromPosition[1] != toPosition[1] :
        if currentPosition[0] < toPosition[0]:
            currentPosition[0] = currentPosition[0] +1
        if currentPosition[0] > toPosition[0]:
            currentPosition[0] = currentPosition[0] -1
        if currentPosition[1] < toPosition[1]:
            currentPosition[1] = currentPosition[1] +1
        if currentPosition[1] > toPosition[1]:
            currentPosition[1] = currentPosition[1] -1
        target.write(ecodes.EV_ABS, ecodes.ABS_X, currentPosition[0])
        target.write(ecodes.EV_ABS, ecodes.ABS_Y, currentPosition[1])
        target.syn()
        time.sleep(0.001) # x1000 = 1 second

def failAnimation(originalPosition):
    for i in range(0, 50):
        target.write(ecodes.EV_ABS, ecodes.ABS_X, originalPosition[0]+int(20*math.cos(i)))
        target.write(ecodes.EV_ABS, ecodes.ABS_Y, originalPosition[1]+int(20*math.sin(i)))
        target.syn()
        time.sleep(0.01) # x1000 = 1 second

def saveCalibration(devpath, key, pCenter, pInputWindow):
    # save a general file, available after boot
    if not os.path.exists("/userdata/system/configs/gun-calibrator"):
        os.makedirs("/userdata/system/configs/gun-calibrator")
    with open('/userdata/system/configs/gun-calibrator/{}.conf'.format(key), 'w') as fd:
        fd.write("{} {} {} {}".format(pCenter[0], pCenter[1], pInputWindow[0], pInputWindow[1]))
    # save for multiple guns, not available after boot
    with open('/var/run/gun-calibrator.conf.{}'.format(os.path.basename(devpath)), 'w') as fd:
        fd.write("{} {} {} {}".format(pCenter[0], pCenter[1], pInputWindow[0], pInputWindow[1]))

def readCalibration(devpath, key, pCenter, pInputWindow):
    try:
        path = None
        # prefer the on configured for the given input
        # otherwise prefer the system one (common in case you've several same devices)
        pathLast   = '/var/run/gun-calibrator.conf.{}'.format(os.path.basename(devpath))
        pathSystem = '/userdata/system/configs/gun-calibrator/{}.conf'.format(key)
        if os.path.exists(pathLast):
            path = pathLast
        elif os.path.exists(pastSystem):
            path = pathSystem
        else:
            return pCenter, pInputWindow
        
        with open(path, 'r') as fd:
            content = fd.readlines()
        vals = content[0].split()

        if vals[2] == 0 or vals[3] == 0: # invalid values
            return pCenter, pInputWindow

        return [int(vals[0]), int(vals[1])], [int(vals[2]), int(vals[3])]
    except:
        return pCenter, pInputWindow

pCenter, pInputWindow = readCalibration(devpath, inputkey, pCenter, pInputWindow)

def calibrationFailed(val):
    if (val[0] == 1 and val[1] == 1023) or (val[0] == 0 and val[1] == 0): # generally, what we get meaning nothing : no point detected
        print("Calibration failed")
        return True
    # should we care about other points if too distant from the first one ?
    print("calibration with x={}, y={}".format(val[0], val[1]))
    return False

try:
        while True:
            if poll.poll(1000):
                if calibration_mode:
                        # read positions during calibration
                        for event in input.read():
                            if event.type == ecodes.EV_ABS and event.code == ecodes.ABS_X:
                                pLast[0] = event.value
                            elif event.type == ecodes.EV_ABS and event.code == ecodes.ABS_Y:
                                pLast[1] = event.value
                            elif event.type == ecodes.EV_KEY and event.code == ecodes.BTN_LEFT and event.value == 1:
                                if calibration_step == 1:
                                    if calibrationFailed(pLast):
                                        calibration_step = 0
                                        calibration_mode = False
                                        failAnimation([outputSize[0]//2, outputSize[1]//2])
                                        target.write(ecodes.EV_KEY, ecodes.KEY_CONFIG, 0)
                                        target.syn()
                                    else:
                                        pCenter = [pLast[0]-(absVals[0][1] - absVals[0][0])//2, pLast[1]-(absVals[1][1] - absVals[1][0])//2]
                                        calibration_step = 2
                                    moveTargetTo([outputSize[0]//2, outputSize[1]//2], [0, 0])
                                elif calibration_step == 2:
                                    if calibrationFailed(pLast):
                                        # hum, retry in step 3
                                        calibration_step = 3
                                        moveTargetTo([0, 0], [outputSize[0]//4, outputSize[1]//4])
                                    else:
                                        pInputWindow = [((absVals[0][1] - absVals[0][0])//2 + pCenter[0] - pLast[0])*2, ((absVals[1][1] - absVals[1][0])//2 + pCenter[1] - pLast[1])*2]
                                        if pInputWindow[0] == 0 or pInputWindow[1] == 0: # calibration failed, reset to defaults
                                            pInputWindow = [absVals[0][1]-absVals[0][0], absVals[1][1]-absVals[1][0]]
                                        calibration_step = 0
                                        calibration_mode = False
                                        saveCalibration(devpath, inputkey, pCenter, pInputWindow)
                                        target.write(ecodes.EV_KEY, ecodes.KEY_CONFIG, 0)
                                        target.syn()
                                elif calibration_step == 3: # step 3 in case of error (not far enough from the tv)
                                    if calibrationFailed(pLast):
                                        pInputWindow = [absVals[0][1]-absVals[0][0], absVals[1][1]-absVals[1][0]]
                                        calibration_step = 0
                                        calibration_mode = False
                                        failAnimation([outputSize[0]//4, outputSize[1]//4])
                                        target.write(ecodes.EV_KEY, ecodes.KEY_CONFIG, 0)
                                        target.syn()
                                    else:
                                        pInputWindow = [((absVals[0][1] - absVals[0][0])//2 + pCenter[0] - (pLast[0]-((absVals[0][1] - absVals[0][0])//2+pCenter[0]-pLast[0])))*2, ((absVals[1][1] - absVals[1][0])//2 + pCenter[1] - (pLast[1]-((absVals[1][1] - absVals[1][0])//2+pCenter[1]-pLast[1])))*2]
                                        if pInputWindow[0] == 0 or pInputWindow[1] == 0: # calibration failed, reset to defaults
                                            pInputWindow = [absVals[0][1]-absVals[0][0], absVals[1][1]-absVals[1][0]]
                                        calibration_step = 0
                                        calibration_mode = False
                                        saveCalibration(devpath, inputkey, pCenter, pInputWindow)
                                        target.write(ecodes.EV_KEY, ecodes.KEY_CONFIG, 0)
                                        target.syn()
                            else:
                                target.write(event.type, event.code, event.value)
                                target.syn()
                else:
                    if calibration_timeChecker is not None:
                        if time.time() - calibration_timeChecker > 3: # 3 seconds
                            calibration_mode = True
                            calibration_timeChecker  = None
                            calibration_step = 1
                            target.write(ecodes.EV_KEY, ecodes.KEY_CONFIG, 1)
                            target.syn()
                            moveTargetTo([outputSize[0]//2, 0], [outputSize[0]//2, outputSize[1]//2])
        
                    # could be enabled by the timeChecker
                    if calibration_mode is False:
                        # normal event read
                        for event in input.read():
                            if event.type == ecodes.EV_ABS and event.code == ecodes.ABS_X:
                                val = int((event.value - pCenter[0] - absVals[0][0] - (absVals[0][1]-absVals[0][0]-pInputWindow[0])//2) * outputSize[0] / pInputWindow[0])
                                if val >= 0 and val <= outputSize[0]: # protect in case
                                    target.write(ecodes.EV_ABS, ecodes.ABS_X, val)
                                needSync = True
                            elif event.type == ecodes.EV_ABS and event.code == ecodes.ABS_Y:
                                val = int((event.value - pCenter[1] - absVals[1][0] - (absVals[1][1]-absVals[1][0]-pInputWindow[1])//2) * outputSize[1] / pInputWindow[1])
                                if val >= 0 and val <= outputSize[1]:
                                    target.write(ecodes.EV_ABS, ecodes.ABS_Y, val)
                                needSync = True
                            elif event.type == ecodes.EV_SYN:
                                if needSync:
                                    target.syn()
                                    needSync = False
                            elif event.type == ecodes.EV_KEY and event.code == bCalibration:
                                if event.value == 1:
                                    calibration_timeChecker = time.time()
                                    target.write(event.type, event.code, event.value)
                                    needSync = True
                                else:
                                    calibration_timeChecker = None
                                    target.write(event.type, event.code, event.value)
                                    needSync = True
                            elif event.type == ecodes.EV_KEY:
                                target.write(event.type, event.code, event.value)
                                needSync = True
except KeyboardInterrupt:
    poll.unregister(input.fd)
    target.close()    
except Exception as e:
    import traceback
    with open('/var/run/gun-calibrator.crash', 'w') as fd:
        fd.write(traceback.format_exc())
    raise e
###
